# -*- coding: utf-8 -*-
# =============================================================================
#     FileName:
#         Desc:
#       Author: GGA
#        Email:
#     HomePage:
#      Version: 1.0.1
#   LastChange: 2020-12-20
#      History:
# =============================================================================
import fire
import time
import redis
import logging
import numpy as np
from collections import namedtuple
from utils.logger_helper import LoggerHelper
from utils.human_helper import HumanHelper

APP_NAME = "redis_expire_helper"
APP_VERSION = "1.0"
logger = LoggerHelper.get_logger(logger_name=APP_NAME)

RedisConfig = namedtuple("RedisConfig", field_names=["redis_host", "redis_port", "redis_password", "redis_db"], )


class RedisExpireTimeChecker(object):
    def __init__(self, key_pattern: str, full_expire_time: int, check_key_count=100000,
                 redis_host="127.0.0.1", redis_port=6379, redis_db=0, redis_password="",
                 batch_scan_size=1000, batch_scan_interval=0.1, batch_pipe_size=100):
        self.key_pattern = key_pattern
        self.full_expire_time = full_expire_time
        self.check_key_count = check_key_count
        self.redis_host = redis_host
        self.redis_port = redis_port
        self.redis_db = redis_db
        self.redis_password = redis_password
        self.batch_scan_size = batch_scan_size
        self.batch_scan_interval = batch_scan_interval
        self.batch_pipe_size = batch_pipe_size
        self.check_data = list()
        self.redis_pool = redis.ConnectionPool(
            host=self.redis_host,
            port=self.redis_port,
            db=self.redis_db,
            password=self.redis_password,
            max_connections=10
        )

    def _get_redis_connection(self):
        """
        从Redis Pool中获取连接，避免重复建立Redis连接
        :return:
        """
        return redis.Redis(connection_pool=self.redis_pool, decode_responses=True)

    def _check_connection(self):
        redis_conn = self._get_redis_connection()
        try:
            redis_conn.ping()
            return True
        except Exception as _:
            logger.info("无法正常连接Redis，请检查Redis配置信息是否正确。")
            exit(1)

    def _scan_keys(self, start_post=0):
        """
        范围扫描匹配的键值, 并返回匹配的KEYS
        :param start_post:  扫描开始位置点
        :return: stop_post,match_keys
            stop_post: 本次扫描的结束位置
            match_keys：满足条件的KEYS
        """
        redis_conn = self._get_redis_connection()
        stop_post, match_keys = redis_conn.scan(start_post, self.key_pattern, self.batch_scan_size)
        logger.info("扫描起始点:{},扫描结束点:{},扫描数量:{},匹配Key数量:{}".format(
            start_post, stop_post, self.batch_scan_size, len(match_keys)))
        return stop_post, match_keys

    def _get_idle_seconds(self, key_names) -> dict:
        key_items = dict()
        if len(key_names) == 0:
            return key_items
        redis_conn = self._get_redis_connection()
        redis_pipe = redis_conn.pipeline()
        for key_name in key_names:
            redis_pipe.object("IDLETIME", key=key_name)
        pipe_result = redis_pipe.execute()
        for key_index, key_name in enumerate(key_names):
            key_items[key_name] = pipe_result[key_index]
        return key_items

    def _get_expire_seconds(self, key_names) -> dict:
        key_items = dict()
        if len(key_names) == 0:
            return key_items
        redis_conn = self._get_redis_connection()
        redis_pipe = redis_conn.pipeline()
        for key_name in key_names:
            redis_pipe.ttl(key_name)
        pipe_result = redis_pipe.execute()
        for key_index, key_name in enumerate(key_names):
            key_items[key_name] = pipe_result[key_index]
        return key_items

    def _spilt_keys_to_pipe_groups(self, key_names):
        """
        将KEY按照pipe操作批次拆分多个pipe组
        :param key_names:
        :return:
        """
        pipe_groups = []
        group_keys = []
        for key_name in key_names:
            group_keys.append(key_name)
            if len(group_keys) == self.batch_pipe_size:
                pipe_groups.append(group_keys)
                group_keys = []
            pass
        if len(group_keys) > 0:
            pipe_groups.append(group_keys)
        return pipe_groups

    def _loop_checker(self):
        logger.info("开始扫描实例")
        start_post = 0
        stop_post = 1
        curr_key_count = 0
        while stop_post > 0 and curr_key_count < self.check_key_count:
            stop_post, match_keys = self._scan_keys(start_post=start_post)
            curr_key_count += len(match_keys)
            batch_groups = self._spilt_keys_to_pipe_groups(key_names=match_keys)
            for batch_group in batch_groups:
                key_names = batch_group
                key_expires = self._get_expire_seconds(key_names=batch_group)
                key_idles = self._get_idle_seconds(key_names=batch_group)
                for key_name in key_names:
                    expire_second = key_expires.get(key_name, None)
                    idle_second = key_idles.get(key_name, None)
                    if expire_second is None or idle_second is None:
                        continue
                    used_second = self.full_expire_time - expire_second - idle_second
                    self.check_data.append(used_second)
            start_post = stop_post
            time.sleep(self.batch_scan_interval)
        logger.info("完成扫描实例")

    def _show_check_results(self):
        print_info = list()
        print_info.append("")
        check_key_count = len(self.check_data)
        print_info.append("采样{}个KEY,采样结果如下:".format(check_key_count))
        for percent_key in [50, 60, 70, 80, 90, 95, 96, 97, 98, 99.00, 99.50, 99.90, 99.99, 99.999]:
            percent_value = round(np.percentile(self.check_data, percent_key), 2)
            percent_count = int(check_key_count * (100 - percent_key) / 100)
            percent_value_human = HumanHelper.convert_second_number(percent_value)
            msg = "{:>10}分位值:      {}(约{}个请求周期超过{})".format(
                percent_key, percent_value, percent_count, percent_value_human
            )
            print_info.append(msg)
        logger.info("\n".join(print_info))

    def check_keys(self):
        self._check_connection()
        self._loop_checker()
        self._show_check_results()


def main(key_pattern: str, full_expire_time: int, check_key_count=100000,
         redis_host="127.0.0.1", redis_port=6379, redis_db=0, redis_password="",
         batch_scan_size=1000, batch_scan_interval=0.1, batch_pipe_size=100):
    LoggerHelper.init_logger(logger_name=APP_NAME, logger_level=logging.INFO)
    global logger
    logger = LoggerHelper.get_logger(logger_name=APP_NAME)
    logger.info("key_pattern:{}\nfull_expire_time:{}\ncheck_key_count:{}".format(
        key_pattern, full_expire_time, check_key_count))
    logger.info("redis_host:{}\nredis_port:{}\nredis_db:{}\nredis_password:{}".format(
        redis_host, redis_port, redis_db, redis_password))
    logger.info("batch_scan_size:{}\nbatch_scan_interval:{}\nbatch_pipe_size:{}\n".format(
        batch_scan_size, batch_scan_interval, batch_pipe_size))
    rtc = RedisExpireTimeChecker(
        key_pattern=key_pattern, full_expire_time=full_expire_time, check_key_count=check_key_count,
        redis_host=redis_host, redis_port=redis_port, redis_db=redis_db, redis_password=redis_password,
        batch_scan_size=batch_scan_size, batch_scan_interval=batch_scan_interval, batch_pipe_size=batch_pipe_size
    )
    rtc.check_keys()


if __name__ == '__main__':
    fire.Fire(main)
